<!DOCTYPE html>
<html>
<head>
    <title>实时语音翻译 - API 2.0 (BCP-47)</title>
    <meta charset="UTF-8">
    <!-- Load kazuki opus.js library -->
    <script src="/libs/opus/libopus.js"></script>
    <script src="/libs/opus/opus.js"></script>

    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f8f9fa;
        }
        .header {
            text-align: center;
            margin-bottom: 30px;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }
        .container {
            display: flex;
            flex-direction: column;
            gap: 20px;
        }
        .controls {
            display: flex;
            gap: 15px;
            align-items: center;
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .language-selector {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        .mode-selector {
            display: flex;
            align-items: center;
            gap: 15px;
            padding: 10px 15px;
            background: #f8f9fa;
            border-radius: 8px;
            border: 2px solid #e9ecef;
        }
        .radio-label {
            display: flex;
            align-items: center;
            gap: 8px;
            cursor: pointer;
            padding: 8px 12px;
            border-radius: 6px;
            transition: all 0.3s ease;
            font-weight: 500;
        }
        .radio-label:hover {
            background-color: #e9ecef;
        }
        .radio-label input[type="radio"],
        .radio-label input[type="checkbox"] {
            margin: 0;
            cursor: pointer;
        }
        .radio-label input[type="radio"]:checked + span,
        .radio-label input[type="checkbox"]:checked + span {
            color: #007bff;
            font-weight: 600;
        }
        .radio-label input[type="checkbox"]:not(:checked) + span {
            color: #6c757d;
            text-decoration: line-through;
        }
        .result-container {
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .result-section {
            margin-bottom: 25px;
        }
        .result-box {
            margin: 10px 0;
            padding: 15px;
            background-color: #f8f9fa;
            border-radius: 8px;
            max-height: 300px;
            overflow-y: auto;
            border: 2px solid #e9ecef;
            font-family: 'Courier New', monospace;
            line-height: 1.4;
        }
        .connection-status {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 12px 16px;
            background: white;
            border-radius: 8px;
            border: 2px solid;
            cursor: pointer;
            transition: all 0.3s ease;
            font-weight: 500;
        }
        .connection-status.connecting {
            border-color: #ffc107;
            color: #856404;
            background-color: #fff3cd;
        }
        .connection-status.connected {
            border-color: #28a745;
            color: #155724;
            background-color: #d4edda;
        }
        .connection-status.disconnected {
            border-color: #dc3545;
            color: #721c24;
            background-color: #f8d7da;
        }
        .status-icon {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            display: inline-block;
            transition: all 0.3s ease;
        }
        .status-icon.connecting {
            background-color: #ffc107;
            animation: pulse 1.5s infinite;
        }
        .status-icon.connected {
            background-color: #28a745;
        }
        .status-icon.disconnected {
            background-color: #dc3545;
        }
        @keyframes pulse {
            0% { opacity: 1; transform: scale(1); }
            50% { opacity: 0.5; transform: scale(1.2); }
            100% { opacity: 1; transform: scale(1); }
        }
        button {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: all 0.3s ease;
        }
        .btn-primary {
            background: linear-gradient(135deg, #007bff, #0056b3);
            color: white;
        }
        .btn-primary:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,123,255,0.3);
        }
        .btn-danger {
            background: linear-gradient(135deg, #dc3545, #c82333);
            color: white;
        }
        .btn-danger:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(220,53,69,0.3);
        }
        button:disabled {
            background-color: #6c757d;
            cursor: not-allowed;
            transform: none;
            box-shadow: none;
        }
        select {
            padding: 10px 15px;
            border-radius: 6px;
            border: 2px solid #dee2e6;
            background-color: white;
            font-size: 14px;
            transition: border-color 0.3s ease;
        }
        select:focus {
            outline: none;
            border-color: #007bff;
        }
        .step-indicator {
            font-size: 12px;
            font-weight: 600;
            color: #6c757d;
            text-transform: uppercase;
            letter-spacing: 1px;
        }
        .asr-result {
            background-color: #e3f2fd;
            border-left: 4px solid #2196f3;
            transition: all 0.3s ease;
        }

        .asr-result.final-result {
            background-color: #e8f5e9;
            border-left: 4px solid #4caf50;
        }

        .asr-text {
            font-size: 16px;
            line-height: 1.5;
            margin-top: 5px;
        }

        .result-item {
            margin-bottom: 8px;
            padding: 12px;
            border-radius: 6px;
            border-left: 4px solid #ddd;
            transition: all 0.3s ease;
        }

        .result-meta {
            font-size: 11px;
            color: #6c757d;
            margin-bottom: 5px;
        }

        .recording-indicator {
            display: none;
            align-items: center;
            gap: 8px;
            color: #dc3545;
            font-weight: 500;
        }
        .recording-indicator.active {
            display: flex;
        }
        .recording-dot {
            width: 8px;
            height: 8px;
            background-color: #dc3545;
            border-radius: 50%;
            animation: blink 1s infinite;
        }
        @keyframes blink {
            0%, 50% { opacity: 1; }
            51%, 100% { opacity: 0.3; }
        }
        .audio-player {
            margin-top: 5px;
        }
        .log-container {
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 6px;
            padding: 10px;
            max-height: 200px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: 12px;
        }

        /* 模式特定样式 */
        .mode-indicator {
            display: inline-block;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 600;
            margin-right: 8px;
        }

        .mode-indicator.fixed {
            background-color: #e3f2fd;
            color: #1976d2;
            border: 1px solid #bbdefb;
        }

        .mode-indicator.auto {
            background-color: #e8f5e9;
            color: #388e3c;
            border: 1px solid #c8e6c9;
        }

        .language-switch-indicator {
            display: inline-block;
            background-color: #fff3cd;
            color: #856404;
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 600;
            margin-left: 8px;
            border: 1px solid #ffeaa7;
        }

        .text-updated {
            background-color: #fff3cd !important;
            transition: background-color 0.3s ease;
        }

        .controls {
            display: flex;
            gap: 15px;
            align-items: center;
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            flex-wrap: wrap; /* 允许换行以容纳更多控件 */
        }

        /* 响应式设计 */
        @media (max-width: 768px) {
            .controls {
                flex-direction: column;
                align-items: stretch;
            }
            .mode-selector {
                justify-content: center;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>🎤 实时语音翻译 - API 2.0 (BCP-47)</h1>
        <p>基于WebSocket的三阶段处理：ASR → 翻译 → TTS • 支持BCP-47国际语言标准 • 支持TTS语音性别选择 • 支持PCM/Raw Opus音频格式 • 可独立开关翻译和TTS功能 • 使用@evan/opus WebAssembly实现真正的Opus编码</p>
    </div>

    <div class="container">
        <!-- 连接状态指示器 -->
        <div id="connectionStatus" class="connection-status disconnected">
            <span id="statusIcon" class="status-icon disconnected"></span>
            <span id="statusText">正在连接服务器...</span>
        </div>

        <!-- 控制面板 -->
        <div class="controls">
            <!-- 模式选择 -->
            <div class="mode-selector">
                <label style="font-weight: bold; color: #495057;">工作模式:</label>
                <div style="display: flex; gap: 15px; align-items: center;">
                    <label class="radio-label">
                        <input type="radio" id="fixedMode" name="workMode" value="fixed" checked>
                        <span>🔒 固定语言对</span>
                    </label>
                    <label class="radio-label">
                        <input type="radio" id="autoMode" name="workMode" value="auto">
                        <span>🤖 自动识别</span>
                    </label>
                </div>
            </div>

            <div class="language-selector">
                <label>原语言:</label>
                <select id="sourceLang">
                    <option value="zh-CN">中文（简体）</option>
                    <option value="en-US">English (US)</option>
                    <option value="ja-JP">日本語</option>
                    <option value="ko-KR">한국어</option>
                    <option value="fr-FR">Français</option>
                    <option value="de-DE">Deutsch</option>
                    <option value="es-ES">Español</option>
                    <option value="it-IT">Italiano</option>
                    <option value="pt-BR">Português</option>
                    <option value="ru-RU">Русский</option>
                    <option value="ar-SA">العربية</option>
                    <option value="hi-IN">हिन्दी</option>
                    <option value="th-TH">ไทย</option>
                </select>
            </div>

            <div class="language-selector">
                <label>目标语言:</label>
                <select id="targetLang">
                    <option value="en-US" selected>English (US)</option>
                    <option value="zh-CN">中文（简体）</option>
                    <option value="ja-JP">日本語</option>
                    <option value="ko-KR">한국어</option>
                    <option value="fr-FR">Français</option>
                    <option value="de-DE">Deutsch</option>
                    <option value="es-ES">Español</option>
                    <option value="it-IT">Italiano</option>
                    <option value="pt-BR">Português</option>
                    <option value="ru-RU">Русский</option>
                    <option value="ar-SA">العربية</option>
                    <option value="hi-IN">हिन्दी</option>
                    <option value="th-TH">ไทย</option>
                </select>
            </div>

            <!-- TTS性别选择 -->
            <div class="language-selector">
                <label>TTS语音:</label>
                <select id="ttsGender">
                    <option value="female" selected>🎙️ 女声</option>
                    <option value="male">🎙️ 男声</option>
                </select>
            </div>

            <!-- ASR格式选择 -->
            <div class="language-selector">
                <label>ASR格式:</label>
                <select id="asrFormat">
                    <option value="pcm" selected>🎵 PCM (原始)</option>
                    <option value="rawopus">🎶 Raw Opus (60ms帧)</option>
                </select>
            </div>

            <!-- 功能开关 -->
            <div class="mode-selector">
                <label style="font-weight: bold; color: #495057;">功能开关:</label>
                <div style="display: flex; gap: 15px; align-items: center;">
                    <label class="radio-label">
                        <input type="checkbox" id="enableTranslation" checked>
                        <span>🌐 启用翻译</span>
                    </label>
                    <label class="radio-label">
                        <input type="checkbox" id="enableTTS" checked>
                        <span>🔊 启用TTS</span>
                    </label>
                </div>
            </div>

            <button id="startButton" class="btn-primary">🎤 开始录音</button>
            <button id="stopButton" class="btn-danger" disabled>⏹️ 停止录音</button>

            <div class="recording-indicator" id="recordingIndicator">
                <div class="recording-dot"></div>
                <span>录音中...</span>
            </div>

            <div class="recording-indicator" id="waitingIndicator" style="color: #007bff; display: none;">
                <div class="recording-dot" style="background-color: #007bff;"></div>
                <span>等待处理完成...</span>
            </div>
        </div>

        <!-- 结果显示区域 -->
        <div class="result-container">
            <!-- ASR结果 -->
            <div class="result-section">
                <h3>🎤 语音识别结果 (ASR)</h3>
                <div id="asrResult" class="result-box"></div>
            </div>

            <!-- 翻译结果 -->
            <div class="result-section">
                <h3>🌐 翻译结果 (TS)</h3>
                <div id="translationResult" class="result-box"></div>
            </div>

            <!-- TTS结果 -->
            <div class="result-section">
                <h3>🔊 语音合成结果 (TTS)</h3>
                <div id="ttsResult" class="result-box"></div>
            </div>

            <!-- 调试日志 -->
            <div class="result-section">
                <h3>📋 调试日志</h3>
                <div id="debugLog" class="log-container"></div>
            </div>
        </div>
    </div>

    <script>
        let ws;
        let audioContext;
        let recording = false;
        let wsConnected = false;
        let mediaStream = null;
        let audioProcessor = null;
        let reconnectTimer = null;
        let currentSession = null;
        let currentAsrSession = null;  // 添加全局变量跟踪当前ASR会话
        let waitingForResults = false;  // 等待最终结果的标志
        let waitingTimer = null;  // 等待超时定时器
        let currentWorkMode = 'fixed';  // 当前工作模式：fixed/auto
        let collectedAudioUrls = [];  // 收集的所有音频URL
        let cachedAudioBlobs = [];  // 缓存的音频Blob数据
        let opusEncoder = null;  // OpusScript编码器实例

        const startButton = document.getElementById('startButton');
        const stopButton = document.getElementById('stopButton');
        const sourceLang = document.getElementById('sourceLang');
        const targetLang = document.getElementById('targetLang');
        const ttsGender = document.getElementById('ttsGender');
        const asrFormat = document.getElementById('asrFormat');
        const enableTranslation = document.getElementById('enableTranslation');
        const enableTTS = document.getElementById('enableTTS');
        const fixedModeRadio = document.getElementById('fixedMode');
        const autoModeRadio = document.getElementById('autoMode');
        const asrResult = document.getElementById('asrResult');
        const translationResult = document.getElementById('translationResult');
        const ttsResult = document.getElementById('ttsResult');
        const statusIcon = document.getElementById('statusIcon');
        const statusText = document.getElementById('statusText');
        const connectionStatus = document.getElementById('connectionStatus');
        const recordingIndicator = document.getElementById('recordingIndicator');
        const waitingIndicator = document.getElementById('waitingIndicator');
        const debugLog = document.getElementById('debugLog');

        startButton.onclick = startRecording;
        stopButton.onclick = stopRecording;

        // 模式切换事件监听器
        fixedModeRadio.addEventListener('change', handleModeChange);
        autoModeRadio.addEventListener('change', handleModeChange);

        // 功能开关事件监听器
        enableTTS.addEventListener('change', handleTTSToggle);
        asrFormat.addEventListener('change', handleASRFormatChange);

        // 点击连接状态区域手动重连
        connectionStatus.onclick = () => {
            if (!wsConnected) {
                manualReconnect();
            }
        };

        // 初始化Opus编码器 - 使用 kazuki opus.js
        async function initOpusEncoder() {
            log('🔧 正在初始化Kazuki Opus编码器...');
            try {
                // kazuki opus.js: 16kHz, 单声道, 60ms帧
                opusEncoder = new OpusEncoder(16000, 1, OpusApplication.Audio, 60);
                log('✅ Kazuki Opus编码器初始化成功！');
                return true;
            } catch (e) {
                log(`❌ Kazuki Opus编码器初始化失败: ${e.message}`);
                console.error("详细错误:", e);
                return false;
            }
        }

        // 使用Opus编码PCM数据 - 使用 kazuki opus.js
        function encodeOpusFrame(pcmFloat32Array) {
            if (!opusEncoder) {
                throw new Error('Opus编码器未初始化');
            }
            // kazuki opus.js 使用 encode_float 方法接收 Float32Array
            // 返回 ArrayBuffer 数组，我们取第一个完整帧
            const opusPackets = opusEncoder.encode_float(pcmFloat32Array);
            if (opusPackets && opusPackets.length > 0) {
                return new Uint8Array(opusPackets[0]); // 返回第一个完整的Opus帧
            }
            return null;
        }

        // 页面加载时建立WebSocket连接
        window.onload = async () => {
            connectWebSocket();
            updateModeDisplay(); // 初始化模式显示
            handleTTSToggle(); // 初始化TTS开关状态
            handleASRFormatChange(); // 初始化ASR格式状态

            // 初始化Opus编码器
            await initOpusEncoder();

            log(`✅ Raw Opus格式支持：前端使用Kazuki Opus.js WebAssembly进行真正的Opus编码`);
        };

        function log(message) {
            const now = new Date();
            const timestamp = now.toLocaleTimeString('zh-CN', { hour12: false }) + '.' + now.getMilliseconds().toString().padStart(3, '0');
            const logEntry = document.createElement('div');
            logEntry.innerHTML = `<span style="color: #6c757d;">[${timestamp}]</span> ${message}`;
            debugLog.appendChild(logEntry);
            debugLog.scrollTop = debugLog.scrollHeight;
            console.log(`[${timestamp}] ${message}`);
        }

        function handleModeChange() {
            const newMode = fixedModeRadio.checked ? 'fixed' : 'auto';

            if (newMode === currentWorkMode) {
                return; // 模式没有变化
            }

            const oldMode = currentWorkMode;
            currentWorkMode = newMode;

            log(`🔄 模式切换: ${oldMode} → ${currentWorkMode}`);

            // 如果正在录音，需要先停止当前会话，然后重新开始
            if (recording) {
                log('⚠️ 检测到录音中的模式切换，重新建立会话...');

                // 结束当前会话
                if (currentSession && wsConnected) {
                    sendEndMessage(currentSession);
                    log('📤 发送结束报文以结束当前会话');
                }

                // 重置会话状态
                currentSession = null;
                currentAsrSession = null;

                // 等待一小段时间后重新开始
                setTimeout(() => {
                    if (recording && wsConnected) {
                        currentSession = sendStartMessage();
                        if (currentSession) {
                            log('✅ 成功重新建立录音会话');
                        } else {
                            log('❌ 重新建立会话失败，停止录音');
                            stopRecording();
                        }
                    }
                }, 100);
            }

            // 更新UI状态
            updateModeDisplay();
        }

        function updateModeDisplay() {
            const modeText = currentWorkMode === 'fixed' ? '固定语言对模式' : '自动识别模式';
            const modeIcon = currentWorkMode === 'fixed' ? '🔒' : '🤖';

            // 在自动模式下，原语言选择可能不太重要，但仍然保持可选
            sourceLang.disabled = false; // 保持启用，作为候选语言之一
            targetLang.disabled = false;

            // 更新页面标题显示当前模式
            const headerTitle = document.querySelector('.header h1');
            headerTitle.innerHTML = `🎤 实时语音翻译 - API 2.0 (BCP-47) ${modeIcon}`;

            log(`📊 当前模式: ${modeIcon} ${modeText}`);
        }

        function handleTTSToggle() {
            const isEnabled = enableTTS.checked;

            // 根据TTS开关状态控制语音性别选择器
            ttsGender.disabled = !isEnabled;

            // 更新样式
            const genderSelector = ttsGender.parentElement;
            if (isEnabled) {
                genderSelector.style.opacity = '1';
                genderSelector.title = '';
                log('🔊 TTS已启用，语音性别选择器可用');
            } else {
                genderSelector.style.opacity = '0.5';
                genderSelector.title = 'TTS已禁用，语音性别选择器不可用';
                log('🚫 TTS已禁用，语音性别选择器不可用');
            }
        }

        function handleASRFormatChange() {
            const selectedFormat = asrFormat.value;
            let formatIcon, formatName, formatTitle;

            if (selectedFormat === 'pcm') {
                formatIcon = '🎵';
                formatName = 'PCM (原始音频)';
                formatTitle = 'PCM: 原始音频格式，质量最高，文件较大，兼容性最好';
            } else if (selectedFormat === 'rawopus') {
                formatIcon = '🎶';
                formatName = 'Raw Opus (60ms原始帧)';
                formatTitle = 'Raw Opus: 前端发送60ms PCM数据帧，后端进行Opus编码，高效传输';
            }

            // 添加格式说明
            const asrSelector = asrFormat.parentElement;
            asrSelector.title = formatTitle;

            log(`🎵 ASR格式已设置为: ${formatIcon} ${formatName}`);
        }

        function updateConnectionStatus(status) {
            log(`连接状态更新: ${status}`);
            statusIcon.className = `status-icon ${status}`;
            connectionStatus.className = `connection-status ${status}`;

            switch(status) {
                case 'connecting':
                    statusText.textContent = '正在连接服务器...';
                    break;
                case 'connected':
                    statusText.textContent = '已连接到服务器 (BCP-47 API 2.0)';
                    break;
                case 'disconnected':
                    statusText.textContent = '连接已断开，点击重连';
                    break;
            }
        }

        function manualReconnect() {
            log('手动重连');
            connectWebSocket();
        }

        function connectWebSocket() {
            if (reconnectTimer) {
                clearTimeout(reconnectTimer);
                reconnectTimer = null;
            }

            if (ws) {
                ws.close();
                ws = null;
            }

            wsConnected = false;
            updateConnectionStatus('connecting');

            try {
                // 自动检测协议：HTTPS使用wss，HTTP使用ws
                const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
                ws = new WebSocket(protocol + '//' + window.location.host + '/ws');

                ws.onopen = () => {
                    log('WebSocket连接成功');
                    wsConnected = true;
                    updateConnectionStatus('connected');

                    // 如果当前正在录音，重新发送开始报文恢复会话
                    if (recording && !currentSession) {
                        log('检测到正在录音但会话已断开，重新发送开始报文');
                        currentSession = sendStartMessage();
                        if (!currentSession) {
                            log('❌ 重新发送开始报文失败，停止录音');
                            stopRecording();
                        } else {
                            log('✅ 成功恢复录音会话');
                        }
                    }
                };

                ws.onmessage = handleWebSocketMessage;

                ws.onerror = (error) => {
                    log(`WebSocket连接错误: ${error}`);
                };

                ws.onclose = (event) => {
                    log(`WebSocket连接关闭: code=${event.code}, reason=${event.reason}`);
                    wsConnected = false;
                    updateConnectionStatus('disconnected');

                    // 连接断开时清除当前会话，以便重连时能正确恢复
                    if (recording && currentSession) {
                        log('连接断开，清除会话状态，重连时将恢复');
                        currentSession = null;
                    }

                    if (event.code !== 1000) {
                        log('3秒后自动重连...');
                        reconnectTimer = setTimeout(() => {
                            connectWebSocket();
                        }, 3000);
                    }
                };

            } catch (error) {
                log(`创建WebSocket失败: ${error}`);
                updateConnectionStatus('disconnected');
                reconnectTimer = setTimeout(() => {
                    connectWebSocket();
                }, 3000);
            }
        }

        // BCP-47语言代码映射显示名称（用于日志显示）
        const LANGUAGE_DISPLAY_NAMES = {
            "zh-CN": "中文（简体）",
            "zh-TW": "中文（繁体）",
            "zh-HK": "中文（香港）",
            "en-US": "English (US)",
            "en-GB": "English (UK)",
            "ja-JP": "日本語",
            "ko-KR": "한국어",
            "fr-FR": "Français",
            "de-DE": "Deutsch",
            "es-ES": "Español",
            "it-IT": "Italiano",
            "pt-BR": "Português",
            "ru-RU": "Русский",
            "ar-SA": "العربية",
            "hi-IN": "हिन्दी",
            "th-TH": "ไทย"
        };

        // 获取语言显示名称
        function getLanguageDisplayName(bcp47Code) {
            return LANGUAGE_DISPLAY_NAMES[bcp47Code] || bcp47Code;
        }

        function validateBCP47Code(code) {
            // 简单的BCP-47格式验证
            const bcp47Pattern = /^[a-z]{2,3}(-[A-Z]{2})?$/;
            return bcp47Pattern.test(code);
        }

        function generateSequence() {
            const fromLang = sourceLang.value;  // 直接使用BCP-47代码
            const toLang = targetLang.value;    // 直接使用BCP-47代码
            const randomHex = Array.from({length: 32}, () => Math.floor(Math.random() * 16).toString(16).toUpperCase()).join('');
            const timestamp = Date.now();
            return `${fromLang}|${toLang}|${randomHex}|${timestamp}`;
        }

        function sendStartMessage() {
            if (!wsConnected || !ws || ws.readyState !== WebSocket.OPEN) {
                log('WebSocket未连接，无法发送开始报文');
                return null;
            }

            // 验证语言代码格式
            if (!validateBCP47Code(sourceLang.value)) {
                log(`❌ 源语言代码格式错误: ${sourceLang.value}`);
                return null;
            }
            if (!validateBCP47Code(targetLang.value)) {
                log(`❌ 目标语言代码格式错误: ${targetLang.value}`);
                return null;
            }

            const seq = generateSequence();

            // 根据工作模式设置detectMode
            // fixed模式: detectMode = 2 (指定语言为from)
            // auto模式: detectMode = 1 (指定语言为from或to，支持自动检测)
            const detectMode = currentWorkMode === 'fixed' ? 2 : 1;

            const startMessage = {
                businessType: "ENTITY_TRANS",
                auto: 1,
                to: targetLang.value,   // 目标语言
                detectMode: detectMode, // 根据模式设置检测模式
                mockType: 1,
                sound: 2,
                tsEnable: enableTranslation.checked ? 1 : 0,  // 是否启用翻译
                action: "SATS",
                from: sourceLang.value, // 源语言
                flag: "start",
                ttsFormat: "mp3",
                ttsEnable: enableTTS.checked ? 1 : 0,         // 是否启用TTS
                ttsGender: ttsGender.value, // TTS语音性别
                seq: seq,
                asrFormat: asrFormat.value, // 🎵 使用用户选择的ASR格式
                flow: 1
            };

            // 在自动模式下，可以添加候选语言（如果后端支持）
            if (currentWorkMode === 'auto') {
                // 构建候选语言数组，包含当前选择的源语言和目标语言
                const candidateLanguages = [sourceLang.value, targetLang.value];
                // 添加一些常用语言作为候选
                const commonLanguages = ['zh-CN', 'en-US', 'ja-JP', 'ko-KR', 'fr-FR', 'de-DE'];
                commonLanguages.forEach(lang => {
                    if (!candidateLanguages.includes(lang)) {
                        candidateLanguages.push(lang);
                    }
                });
                startMessage.candidateLanguages = candidateLanguages;
            }

            ws.send(JSON.stringify(startMessage));

            const modeDesc = currentWorkMode === 'fixed'
                ? `固定模式 (detectMode=${detectMode})`
                : `自动模式 (detectMode=${detectMode}, 候选语言: ${startMessage.candidateLanguages ? startMessage.candidateLanguages.join(', ') : '无'})`;

            const genderIcon = ttsGender.value === 'male' ? '👨' : '👩';
            const genderText = ttsGender.value === 'male' ? '男声' : '女声';

            // 构建功能状态描述
            const translationStatus = enableTranslation.checked ? '🌐启用翻译' : '🚫禁用翻译';
            const ttsStatus = enableTTS.checked ? `🔊启用TTS(${genderIcon}${genderText})` : '🚫禁用TTS';
            const formatIcon = asrFormat.value === 'pcm' ? '🎵' : '🎶';
            const formatName = asrFormat.value === 'rawopus' ? 'Raw Opus' : asrFormat.value.toUpperCase();
            const asrStatus = `${formatIcon}ASR格式=${formatName}`;

            log(`🚀 发送BCP-47开始报文: from=${getLanguageDisplayName(sourceLang.value)} (${sourceLang.value}), to=${getLanguageDisplayName(targetLang.value)} (${targetLang.value}), ${translationStatus}, ${ttsStatus}, ${asrStatus}, 模式=${modeDesc}, seq=${seq}`);

            return seq;
        }

        function sendEndMessage(seq) {
            if (!wsConnected || !ws || ws.readyState !== WebSocket.OPEN) {
                log('WebSocket未连接，无法发送结束报文');
                return;
            }

            const endMessage = {
                seq: seq,
                action: "SATS",
                ttsFormat: "mp3",
                flag: "end"
            };

            ws.send(JSON.stringify(endMessage));
            log(`发送结束报文: seq=${seq}`);
        }

        function startRecording() {
            if (recording) return;

            if (!wsConnected) {
                alert('WebSocket连接未建立，请稍后再试');
                return;
            }

            // 验证选择的音频格式
            const selectedFormat = asrFormat.value;
            if (selectedFormat === 'rawopus') {
                if (!opusEncoder) {
                    log(`⚠️ @evan/opus编码器未初始化，自动切换到PCM格式`);
                    asrFormat.value = 'pcm';
                    handleASRFormatChange();
                    alert('Opus编码器未初始化，已自动切换到PCM格式。请确保网络连接正常并刷新页面重试。');
                    return;
                } else {
                    log(`🎶 Raw Opus模式：将使用@evan/opus编码60ms PCM数据帧`);
                }
            }

            // 重置ASR会话
            currentAsrSession = null;

            // 清空之前的结果
            asrResult.innerHTML = '';
            translationResult.innerHTML = '';
            ttsResult.innerHTML = '';

            // 清空收集的音频URL和缓存的音频数据
            collectedAudioUrls = [];
            cachedAudioBlobs = [];

            // 根据功能开关状态显示提示信息
            if (!enableTranslation.checked) {
                const infoElement = document.createElement('div');
                infoElement.className = 'result-item';
                infoElement.style.backgroundColor = '#fff3cd';
                infoElement.style.borderLeft = '4px solid #ffc107';
                infoElement.style.color = '#856404';
                infoElement.innerHTML = '<div class="result-meta">功能提示</div><div>🚫 翻译功能已禁用，将不会收到翻译结果</div>';
                translationResult.appendChild(infoElement);
            }

            if (!enableTTS.checked) {
                const infoElement = document.createElement('div');
                infoElement.className = 'result-item';
                infoElement.style.backgroundColor = '#fff3cd';
                infoElement.style.borderLeft = '4px solid #ffc107';
                infoElement.style.color = '#856404';
                infoElement.innerHTML = '<div class="result-meta">功能提示</div><div>🚫 TTS功能已禁用，将不会收到语音合成结果</div>';
                ttsResult.appendChild(infoElement);
            }

            // 发送开始报文
            currentSession = sendStartMessage();
            if (!currentSession) {
                alert('发送开始报文失败');
                return;
            }

            // 获取麦克风权限
            navigator.mediaDevices.getUserMedia({
                audio: {
                    sampleRate: 16000,
                    channelCount: 1,
                    echoCancellation: true,
                    noiseSuppression: true
                }
            })
            .then(stream => {
                recording = true;
                mediaStream = stream;
                startButton.disabled = true;
                stopButton.disabled = false;
                recordingIndicator.classList.add('active');

                log('开始录音，采样率: 16kHz');

                // 🎵 根据选择的音频格式使用不同的处理方式
                const selectedFormat = asrFormat.value;

                // 统一使用AudioContext进行音频处理
                audioContext = new AudioContext({ sampleRate: 16000 });
                const source = audioContext.createMediaStreamSource(stream);

                if (selectedFormat === 'rawopus') {
                    // 🎶 Raw Opus格式：收集60ms PCM数据，使用@evan/opus编码为真正的Opus帧
                    log(`🎶 Raw Opus模式：收集60ms PCM数据并编码为真正的Opus帧`);

                    if (!opusEncoder) {
                        log(`❌ @evan/opus编码器未初始化，无法使用Raw Opus模式`);
                        alert('Opus编码器未初始化，请刷新页面重试或选择PCM格式');
                        stopRecording();
                                            return;
                                        }

                    // 60ms @ 16kHz = 960个样本
                    const FRAME_SIZE = 960;
                    let frameBuffer = new Float32Array(FRAME_SIZE);
                    let frameIndex = 0;
                    let frameCount = 0;
                    let totalOpusBytes = 0;

                    audioProcessor = audioContext.createScriptProcessor(1024, 1, 1);

                    audioProcessor.onaudioprocess = (e) => {
                        if (recording && wsConnected && ws.readyState === WebSocket.OPEN && currentSession) {
                            const inputData = e.inputBuffer.getChannelData(0);

                            // 将输入数据添加到帧缓冲区
                            for (let i = 0; i < inputData.length; i++) {
                                if (frameIndex < FRAME_SIZE) {
                                    frameBuffer[frameIndex++] = inputData[i];

                                    // 当收集满60ms数据时，编码并发送
                                    if (frameIndex === FRAME_SIZE) {
                                        try {
                                            // **修正: 直接将Float32Array传递给编码函数**
                                            const opusFrame = encodeOpusFrame(frameBuffer);

                                            if (opusFrame) {
                                                ws.send(opusFrame); // kazuki opus.js 返回 Uint8Array，直接发送
                                                frameCount++;
                                                totalOpusBytes += opusFrame.length;

                                                // 每10帧记录一次
                                                if (frameCount % 10 === 0) {
                                                    const avgSize = Math.round(totalOpusBytes / frameCount);
                                                    const compressionRatio = Math.round((FRAME_SIZE * 2) / avgSize * 100) / 100;
                                                    log(`📡 Raw Opus帧 #${frameCount}: 60ms编码完成 (${opusFrame.length}字节, 平均${avgSize}字节, 压缩比${compressionRatio}:1)`);
                                                }
                                            }
                                        } catch (encodeError) {
                                            log(`❌ Opus编码失败: ${encodeError.message}`);
                                        }

                                        // 重置帧缓冲区
                                        frameIndex = 0;
                                    }
                                }
                            }
                        }
                    };

                } else {
                    // 🎵 PCM格式：使用传统的连续发送方式
                    log(`🎵 PCM模式：连续发送音频数据`);

                    audioProcessor = audioContext.createScriptProcessor(1024, 1, 1);
                    let audioChunkCount = 0;

                    audioProcessor.onaudioprocess = (e) => {
                        if (recording && wsConnected && ws.readyState === WebSocket.OPEN && currentSession) {
                            const inputData = e.inputBuffer.getChannelData(0);
                            const intData = new Int16Array(inputData.length);
                            for (let i = 0; i < inputData.length; i++) {
                                intData[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32767));
                            }
                            ws.send(intData.buffer);

                            audioChunkCount++;
                            if (audioChunkCount % 50 === 0) {  // 每50个音频块记录一次
                                log(`📡 PCM音频数据 #${audioChunkCount}: ${intData.length}样本 (${intData.buffer.byteLength}字节)`);
                            }
                        } else if (recording && (!wsConnected || !currentSession)) {
                            // 录音中但连接断开或会话无效，等待重连恢复
                            if (audioChunkCount % 100 === 0) {  // 每100个音频块记录一次
                                log(`⏸️ 等待连接恢复... (wsConnected=${wsConnected}, currentSession=${!!currentSession})`);
                            }
                            audioChunkCount++;
                        }
                    };
                }

                    source.connect(audioProcessor);
                    audioProcessor.connect(audioContext.destination);
            })
            .catch(err => {
                log(`获取麦克风失败: ${err}`);
                alert('无法访问麦克风，请检查权限设置');
                recording = false;
            });
        }

        function handleWebSocketMessage(event) {
            try {
                const response = JSON.parse(event.data);

                // 检查是否是会话结束响应
                if (response.code === 2025) {
                    log(`🏁 收到会话结束响应: ${response.msg} (code: ${response.code})`);
                    handleSessionEndResponse(response);
                    return;
                }

                log(`收到${response.step || '未知'}响应: code=${response.code}, msg=${response.msg}`);

                // 检查是否是错误响应
                if (response.code !== 0) {
                    log(`❌ 服务器错误: ${response.msg} (code: ${response.code})`);

                    // 如果是会话状态错误且正在录音，尝试恢复会话
                    if (response.code === 504 && recording) {
                        log('🔄 检测到会话状态错误，尝试恢复会话');
                        if (wsConnected && !currentSession) {
                            currentSession = sendStartMessage();
                            if (currentSession) {
                                log('✅ 成功恢复会话');
                            } else {
                                log('❌ 恢复会话失败，停止录音');
                                stopRecording();
                            }
                        }
                    }
                    return;
                }

                switch(response.step) {
                    case 'ASR':
                        handleASRResponse(response);
                        break;
                    case 'TS':
                        handleTSResponse(response);
                        break;
                    case 'TTS':
                        handleTTSResponse(response);
                        break;
                    default:
                        log(`未知响应类型: ${response.step}`);
                }
            } catch (err) {
                log(`解析响应消息失败: ${err}`);
            }
        }

        function handleASRResponse(response) {
            // 详细记录ASR响应信息
            let logMessage = `🎤 ASR响应详情: asrSid=${response.asrSid}, duration=${response.duration}ms, offset=${response.offset}ms, isLast=${response.isLast}`;

            // 在自动模式下显示语言检测信息
            if (currentWorkMode === 'auto' && response.detected_language) {
                logMessage += `, 检测语言=${getLanguageDisplayName(response.detected_language)} (${response.detected_language})`;
            }
            if (response.langSwitched) {
                logMessage += `, 🔄 语言切换检测`;
            }
            if (response.confidence) {
                logMessage += `, 置信度=${response.confidence}`;
            }

            log(logMessage);

            if (response.code !== 0) {
                log(`❌ ASR错误: ${response.msg} (code: ${response.code})`);
                return;
            }

            // 记录ASR识别内容，即使为空也要显示
            if (response.text && response.text.trim() !== '') {
                log(`🎯 ASR识别内容: "${response.text}" ${response.isLast ? '[最终]' : '[中间]'}`);

                let resultElement;

                // 如果是新的ASR会话，创建新元素
                if (currentAsrSession !== response.asrSid) {
                    currentAsrSession = response.asrSid;

                    resultElement = document.createElement('div');
                    resultElement.className = 'result-item asr-result';
                    resultElement.setAttribute('data-asr-sid', response.asrSid);

                    const meta = document.createElement('div');
                    meta.className = 'result-meta';
                    resultElement.appendChild(meta);

                    const text = document.createElement('div');
                    text.className = 'asr-text';
                    resultElement.appendChild(text);

                    // 添加一个淡入效果
                    resultElement.style.opacity = '0';
                    asrResult.appendChild(resultElement);
                    setTimeout(() => {
                        resultElement.style.opacity = '1';
                    }, 10);
                } else {
                    // 使用现有元素
                    resultElement = asrResult.querySelector(`[data-asr-sid="${response.asrSid}"]`);
                }

                if (resultElement) {
                    // 构建元数据信息
                    let metaInfo = `ASR ID: ${response.asrSid} | ${response.isLast ? '最终结果' : '中间结果'} | 时长: ${response.duration}ms`;

                    // 在自动模式下添加语言检测信息
                    if (currentWorkMode === 'auto') {
                        const modeIcon = '🤖';
                        metaInfo = `${modeIcon} 自动模式 | ${metaInfo}`;

                        if (response.detected_language) {
                            metaInfo += ` | 🗣️ 检测: ${getLanguageDisplayName(response.detected_language)}`;
                        }
                        if (response.langSwitched) {
                            metaInfo += ` | 🔄 语言切换`;
                        }
                    } else {
                        const modeIcon = '🔒';
                        metaInfo = `${modeIcon} 固定模式 | ${metaInfo}`;
                    }

                    // 更新元素内容
                    const meta = resultElement.querySelector('.result-meta');
                    meta.innerHTML = metaInfo;

                    const text = resultElement.querySelector('.asr-text');

                    // 如果文本内容有变化，添加一个高亮效果
                    if (text.textContent !== response.text) {
                        text.textContent = response.text;
                        text.classList.add('text-updated');
                        setTimeout(() => {
                            text.classList.remove('text-updated');
                        }, 300);
                    }

                    // 如果是最终结果，添加样式标记并开始新的会话
                    if (response.isLast) {
                        resultElement.classList.add('final-result');
                        currentAsrSession = null;  // 重置当前会话，准备接收新的会话
                    }

                    asrResult.scrollTop = asrResult.scrollHeight;
                }
            } else {
                log(`📝 ASR处理中: 暂无识别结果 ${response.isLast ? '[最终]' : '[中间]'}`);
            }
        }

        function handleTSResponse(response) {
            log(`🌐 翻译响应详情: asrSid=${response.asrSid}, 语言${getLanguageDisplayName(response.from)}→${getLanguageDisplayName(response.to)}`);

            if (response.code !== 0) {
                log(`❌ 翻译错误: ${response.msg} (code: ${response.code})`);
                return;
            }

            log(`✅ 翻译成功: "${response.text}"`);

            const resultElement = document.createElement('div');
            resultElement.className = 'result-item ts-result';

            const meta = document.createElement('div');
            meta.className = 'result-meta';

            // 根据当前翻译开关状态显示不同信息
            const translationEnabled = enableTranslation.checked;
            if (translationEnabled) {
                meta.innerHTML = `ASR ID: ${response.asrSid} | 语言: ${getLanguageDisplayName(response.from)} → ${getLanguageDisplayName(response.to)} | ✅ 翻译已启用`;
            } else {
                meta.innerHTML = `ASR ID: ${response.asrSid} | 语言: ${getLanguageDisplayName(response.from)} → ${getLanguageDisplayName(response.to)} | ⚠️ 翻译应已禁用但仍收到响应`;
            }

            const text = document.createElement('div');
            text.textContent = response.text;

            resultElement.appendChild(meta);
            resultElement.appendChild(text);
            translationResult.appendChild(resultElement);
            translationResult.scrollTop = translationResult.scrollHeight;
        }

        function handleTTSResponse(response) {
            const currentGender = ttsGender.value;
            const genderIcon = currentGender === 'male' ? '👨' : '👩';
            const genderText = currentGender === 'male' ? '男声' : '女声';

            log(`🔊 TTS响应详情: asrSid=${response.asrSid}, 目标语言=${getLanguageDisplayName(response.to)}, 语音=${genderIcon}${genderText}`);

            if (response.code !== 0) {
                log(`❌ TTS错误: ${response.msg} (code: ${response.code})`);
                return;
            }

            log(`🎵 TTS完成，音频URL: ${response.url}`);

            // 立即缓存音频文件
            cacheAudioFile(response.url, response.asrSid, response.to);

            const resultElement = document.createElement('div');
            resultElement.className = 'result-item tts-result';

            const meta = document.createElement('div');
            meta.className = 'result-meta';

            // 根据当前TTS开关状态显示不同信息
            const ttsEnabled = enableTTS.checked;
            if (ttsEnabled) {
                meta.innerHTML = `ASR ID: ${response.asrSid} | 目标语言: ${getLanguageDisplayName(response.to)} | 语音: ${genderIcon}${genderText} | ✅ TTS已启用`;
            } else {
                meta.innerHTML = `ASR ID: ${response.asrSid} | 目标语言: ${getLanguageDisplayName(response.to)} | 语音: ${genderIcon}${genderText} | ⚠️ TTS应已禁用但仍收到响应`;
            }

            const urlDiv = document.createElement('div');
            urlDiv.innerHTML = `音频URL: <a href="${response.url}" target="_blank">${response.url}</a>`;

            const audio = document.createElement('audio');
            audio.controls = true;
            audio.src = response.url;
            audio.className = 'audio-player';

            resultElement.appendChild(meta);
            resultElement.appendChild(urlDiv);
            resultElement.appendChild(audio);
            ttsResult.appendChild(resultElement);
            ttsResult.scrollTop = ttsResult.scrollHeight;
        }

        function handleSessionEndResponse(response) {
            log(`🏁 会话结束响应详情: asrSid=${response.asrSid}, seq=${response.seq}`);
            log(`✅ 服务器确认: ${response.msg}`);

            // 在调试日志中显示会话结束信息
            const resultElement = document.createElement('div');
            resultElement.className = 'result-item session-end-result';
            resultElement.style.backgroundColor = '#f0f8ff';
            resultElement.style.borderLeft = '4px solid #1e90ff';

            const meta = document.createElement('div');
            meta.className = 'result-meta';
            meta.innerHTML = `会话结束 | ASR ID: ${response.asrSid} | 状态码: ${response.code}`;

            const text = document.createElement('div');
            text.textContent = response.msg;
            text.style.fontWeight = 'bold';
            text.style.color = '#1e90ff';

            resultElement.appendChild(meta);
            resultElement.appendChild(text);

            // 在翻译结果区域显示会话结束信息
            translationResult.appendChild(resultElement);
            translationResult.scrollTop = translationResult.scrollHeight;

            // 如果正在等待结果，完成停止
            if (waitingForResults) {
                log('🎯 收到会话结束响应，完成停止流程');
                completeStop();
            }
        }

        function stopRecording() {
            if (!recording && !waitingForResults) return;

            log('停止录音');
            recording = false;
            recordingIndicator.classList.remove('active');

            // 停止音频处理
            if (mediaStream) {
                mediaStream.getTracks().forEach(track => track.stop());
                mediaStream = null;
            }

            // 音频格式相关的清理已经在AudioContext中统一处理

            // 🎵 停止AudioContext（PCM/Raw Opus格式通用）
            if (audioProcessor) {
                audioProcessor.disconnect();
                audioProcessor = null;
                log(`🎵 已停止${asrFormat.value.toUpperCase()}音频处理`);
            }

            if (audioContext) {
                audioContext.close();
                audioContext = null;
            }

            // 发送结束报文并等待服务器响应
            if (currentSession && wsConnected) {
                sendEndMessage(currentSession);
                waitingForResults = true;
                waitingIndicator.style.display = 'flex';
                stopButton.disabled = true;
                log('🔄 已发送结束报文，等待3秒以接收完整的ASR、翻译和TTS结果...');

                // 设置超时机制，等待3秒后自动完成停止，给ASR、翻译和TTS足够的处理时间
                waitingTimer = setTimeout(() => {
                    if (waitingForResults) {
                        log('⏰ 等待3秒后自动完成停止，确保ASR、翻译和TTS结果都已接收');
                        completeStop();
                    }
                }, 3000);
            } else {
                // 如果没有活跃会话，直接完成停止
                completeStop();
            }
        }

        async function cacheAudioFile(url, asrSid, to) {
            try {
                log(`📥 开始缓存音频文件: ASR ID ${asrSid}, URL: ${url}`);

                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const blob = await response.blob();
                const blobUrl = URL.createObjectURL(blob);

                // 存储缓存的音频数据
                const audioData = {
                    blobUrl: blobUrl,
                    originalUrl: url,
                    asrSid: asrSid,
                    to: to,
                    blob: blob,
                    size: blob.size
                };

                cachedAudioBlobs.push(audioData);
                collectedAudioUrls.push(audioData);

                log(`✅ 音频缓存完成: ASR ID ${asrSid}, 大小: ${Math.round(blob.size / 1024)}KB, Blob URL: ${blobUrl}`);

                // 预加载音频以进一步提高播放速度
                const audio = new Audio(blobUrl);
                audio.preload = 'auto';
                audio.oncanplaythrough = () => {
                    log(`🚀 音频预加载完成: ASR ID ${asrSid}, 可立即播放`);
                };
                audio.onerror = (error) => {
                    log(`⚠️ 音频预加载失败: ASR ID ${asrSid}, ${error}`);
                };

            } catch (error) {
                log(`❌ 缓存音频文件失败: ASR ID ${asrSid}, 错误: ${error.message}`);

                // 缓存失败时，仍然保存原始URL作为备用
                const fallbackData = {
                    blobUrl: null,
                    originalUrl: url,
                    asrSid: asrSid,
                    to: to,
                    blob: null,
                    size: 0
                };

                collectedAudioUrls.push(fallbackData);
                log(`📋 已保存原始URL作为备用: ASR ID ${asrSid}`);
            }
        }

        function playAllCollectedAudio() {
            if (collectedAudioUrls.length === 0) {
                log('📭 没有音频文件可播放');
                return;
            }

            log(`🎵 准备连续播放 ${collectedAudioUrls.length} 个音频文件 (${cachedAudioBlobs.length} 个已缓存)`);

            let currentIndex = 0;

            function playNext() {
                if (currentIndex >= collectedAudioUrls.length) {
                    log('🎵 所有音频播放完成');
                    return;
                }

                const audioInfo = collectedAudioUrls[currentIndex];
                log(`🎵 正在播放第 ${currentIndex + 1} 个音频: ASR ID ${audioInfo.asrSid}, 语言: ${getLanguageDisplayName(audioInfo.to)}`);

                // 优先使用缓存的Blob URL，如果没有则使用原始URL
                const audioUrl = audioInfo.blobUrl || audioInfo.originalUrl;
                const isCached = !!audioInfo.blobUrl;

                log(`${isCached ? '⚡' : '🌐'} 使用${isCached ? '缓存' : '网络'}音频: ${audioUrl}`);

                const audio = new Audio(audioUrl);

                audio.onended = () => {
                    log(`✅ 第 ${currentIndex + 1} 个音频播放完成`);
                    currentIndex++;
                    // 播放下一个音频，添加短暂延迟以避免播放过于紧密
                    setTimeout(playNext, 200);
                };

                audio.onerror = (error) => {
                    log(`❌ 第 ${currentIndex + 1} 个音频播放失败: ${error}`);

                    // 如果缓存的音频播放失败，尝试使用原始URL
                    if (isCached && audioInfo.originalUrl) {
                        log(`🔄 尝试使用原始URL播放: ${audioInfo.originalUrl}`);
                        const fallbackAudio = new Audio(audioInfo.originalUrl);

                        fallbackAudio.onended = () => {
                            log(`✅ 第 ${currentIndex + 1} 个音频(备用)播放完成`);
                            currentIndex++;
                            setTimeout(playNext, 200);
                        };

                        fallbackAudio.onerror = () => {
                            log(`❌ 第 ${currentIndex + 1} 个音频(备用)也播放失败，跳过`);
                            currentIndex++;
                            setTimeout(playNext, 200);
                        };

                        fallbackAudio.play().catch(() => {
                            currentIndex++;
                            setTimeout(playNext, 200);
                        });
                    } else {
                        currentIndex++;
                        setTimeout(playNext, 200);
                    }
                };

                audio.onloadstart = () => {
                    if (!isCached) {
                        log(`📡 开始加载第 ${currentIndex + 1} 个音频...`);
                    }
                };

                audio.oncanplay = () => {
                    log(`▶️ 第 ${currentIndex + 1} 个音频准备就绪，开始播放`);
                };

                // 开始播放
                audio.play().catch(error => {
                    log(`❌ 播放第 ${currentIndex + 1} 个音频时出错: ${error.message}`);
                    currentIndex++;
                    setTimeout(playNext, 200);
                });
            }

            // 开始播放第一个音频
            playNext();
        }

        function completeStop() {
            log('✅ 录音会话完全结束');

            // 播放所有收集到的音频
            if (collectedAudioUrls.length > 0) {
                log(`🎵 开始播放收集到的 ${collectedAudioUrls.length} 个音频文件`);
                playAllCollectedAudio();

                // 播放完成后清理缓存的Blob URL，避免内存泄漏
                setTimeout(() => {
                    cleanupCachedAudio();
                }, (collectedAudioUrls.length * 5 + 10) * 1000); // 估算播放时间后清理
            } else {
                log('📭 没有收集到音频文件，跳过播放');
            }

            waitingForResults = false;
            waitingIndicator.style.display = 'none';
            startButton.disabled = false;
            stopButton.disabled = true;
            currentSession = null;
            currentAsrSession = null;

            // 清理超时定时器
            if (waitingTimer) {
                clearTimeout(waitingTimer);
                waitingTimer = null;
            }
        }

        function cleanupCachedAudio() {
            log(`🧹 开始清理缓存的音频数据: ${cachedAudioBlobs.length} 个文件`);

            cachedAudioBlobs.forEach((audioData, index) => {
                if (audioData.blobUrl) {
                    URL.revokeObjectURL(audioData.blobUrl);
                    log(`🗑️ 已释放Blob URL ${index + 1}: ${audioData.asrSid}`);
                }
            });

            // 清空缓存数组
            cachedAudioBlobs = [];
            log('✅ 音频缓存清理完成');
        }

        // 清理Opus编码器资源
        function cleanupOpusEncoder() {
            if (opusEncoder) {
                try {
                    // kazuki opus.js 编码器使用 delete 方法清理资源
                    if (typeof opusEncoder.delete === 'function') {
                        opusEncoder.delete();
                    }
                    log('🧹 Kazuki Opus编码器资源已清理');
                } catch (error) {
                    log(`⚠️ 清理Kazuki Opus编码器时出错: ${error.message}`);
                }

                opusEncoder = null;
            }
        }

        // 页面关闭时清理资源
        window.onbeforeunload = () => {
            if (recording) {
                stopRecording();
            }
            if (waitingForResults) {
                waitingForResults = false;
            }
            if (ws) {
                ws.close();
            }
            // 清理缓存的音频数据
            cleanupCachedAudio();
            // 清理OpusScript编码器
            cleanupOpusEncoder();
        };
    </script>
</body>
</html>